上一篇從 OAuth 2.0/OIDC 的授權流程出發,介紹 state、PKCE 與 nonce 如何在不同階段降低回應被注入、授權碼被攔截,以及登入結果被重放的風險。
但假設這些檢查都做對了,使用者也完成 MFA,Client 順利拿到 Access Token,故事就結束了嗎?
還沒有。接下來每一次呼叫 API,Client 都需要把 Access Token 交給 Resource Server。如果這顆 Token 在瀏覽器、伺服器 Log 或遭入侵的執行環境中被偷走,攻擊者可能不需要知道密碼,也不需要重新通過 MFA,就能直接拿它存取資源。
因此,今天要回答的問題是:如果攻擊者只偷到 Access Token,卻沒有對應的 Client 金鑰,系統能不能阻止他直接使用這顆 Token?
本篇會先回到 Bearer Token 的安全模型,再介紹短效期限、Scope 與 Audience 能限制的範圍,最後拆解 mTLS 與 DPoP 這兩種 Sender-Constrained Token 機制。
今天內容涵蓋:
前面介紹 Token-Based Auth 時,曾把 Bearer Token 比喻成一張「持有即可使用」的通行證。這不是單純的比喻,而是 RFC 6750 §1.2 對 Bearer Token 的核心定義:使用者只要持有 Token,就可以在它允許的範圍內使用,不需要另外證明自己持有某把密碼學金鑰。
一般 API 請求會長這樣:
GET /payroll HTTP/1.1
Host: api.example.com
Authorization: Bearer ACCESS_TOKEN
Resource Server 收到請求後,會驗證 Token 是否可信、是否過期、是否適用於這支 API,以及是否具備需要的權限。但在純 Bearer 模型中,它只確認 Token 本身能不能用,不會確認現在拿著 Token 發出請求的人,是否就是當初取得這顆 Token 的 Client。
因此,如果攻擊者取得一顆尚未失效的 Access Token,就可能把它放進攻擊者自己發出的 API 請求中。只要 Resource Server 判定這顆 Token 仍然有效,且權限與使用範圍符合要求,請求就可能被接受。
這裡最容易混淆的是 Token 的格式與 Token 的使用方式:
因此,問題不在於 JWT 能不能驗證簽章,而在於 Bearer Token 的使用方式。攻擊者只要拿到一顆尚未失效的 Access Token,就可以原封不動放進自己的 API 請求中;因為 Token 內容沒有被修改,簽章仍然會通過。
Token 竊取不一定發生在網路傳輸途中,也不一定需要破解 HTTPS。Token 抵達 Client 或 Resource Server 之後,仍然可能因為程式漏洞、紀錄方式或執行環境遭入侵而外洩。
實務上,Access Token 可能出現在不同類型的應用與基礎設施中,例如雲端服務、SaaS 平台、容器環境、CI/CD 流程或內部系統的 API 整合。只要這些環境的紀錄、設定檔、記憶體或執行流程被攻擊者取得,Token 就可能被複製並拿去使用。
可以把常見外洩位置整理成三類:
| 外洩位置 | 可能發生的情況 | 風險重點 |
|---|---|---|
| 瀏覽器/Client | XSS 讀取 JavaScript 可存取的 Token、惡意擴充套件或裝置遭入侵 | Token 已經抵達 Client,風險來自端點或前端程式 |
| Log/除錯與監控系統 | Authorization Header、Token Response 被完整寫入 Log、Trace 或錯誤報告 | 風險來自系統把 Token 記錄下來,或讓它進入可被查詢的監控資料 |
| 伺服器/容器/CI/CD | 程式執行環境遭入侵,Token 從檔案、記憶體或工作流程中被取走 | 風險來自執行環境或憑證管理,而不是單純的網路傳輸 |
例如,一個報表系統的情境可能是:
使用者完成登入與 MFA
↓
Client 取得有效的 Bearer Access Token
↓
除錯程式把完整 Authorization Header 寫入 Log
↓
攻擊者取得 Log 中的 Token
↓
從其他環境帶著同一顆 Token 呼叫 API
↓
若 Token 與存取條件仍有效,API 可能放行
這裡並不是 MFA 被破解,而是攻擊者拿到了 MFA 完成之後核發的存取憑證。如果 API 只要求有效 Token,攻擊者自然不必重新走一次登入流程。
同樣地,PKCE 主要保護 Authorization Code 的交換;它不會自動讓交換完成後的 Access Token 具有防冒用能力。
⚠️這裡討論的是「合法 Client 取得的 Access Token 被複製走」的情境。如果使用者一開始就把權限授予惡意 App,問題就不只是 Token 被偷,而是授權對象本身已經不可信,這時要先避免使用者把權限授予不可信的 App,例如限制哪些 App 可以被授權、檢查 App 要求的權限是否合理,並在授權畫面清楚提醒使用者。
在導入金鑰綁定之前,先把 Token 本身的使用範圍收斂,仍然是最基本的防護。
可以從三個問題來看:可以用多久、可以做什麼,以及可以用在哪裡。
| 限制 | 對應的控制重點 | 能降低的風險 | 無法單獨解決的事 |
|---|---|---|---|
| 有效期限 | 這顆 Token 可以用多久? | 縮短外洩後可以被利用的時間 | 過期之前仍可能被冒用 |
| Scope | 這顆 Token 可以要求哪些操作? | 限制攻擊者取得的權限範圍 | 被允許的操作仍可能由攻擊者執行 |
| Audience | 這顆 Token 是發給哪個 Resource Server 的? | 避免 Token 被拿到非預期的 API 使用 | 攻擊者仍可能拿它呼叫原本的 API |
這些限制都沒有改變 Bearer 的本質:在允許的時間、操作與 API 範圍內,偷到 Token 的人仍可能使用它。
這些限制需要由 Resource Server 實際檢查,才會產生效果;不能只把有效期限、Scope 或 Audience 寫進 Token,就視為已完成防護。有效期限也應依資源敏感度、撤銷能力與使用情境調整,而不是套用固定數字。
縮短 Access Token 效期,只能減少它被偷走後可以使用的時間;但如果攻擊者連 Refresh Token 也取得,就可能繼續換到新的 Access Token。
因此,Refresh Token 也需要保護。常見做法包括:
如果系統發現已失效的 Refresh Token 又被拿來使用,就可以判斷可能發生外洩,並撤銷相關 Token。
需要注意的是,這些機制主要是在保護「後續繼續換新 Token」的能力;已經發出去的 Access Token,仍需要靠有效期限、撤銷機制或 Resource Server 的檢查來限制風險。
前三節先說明了 Bearer Token 的風險:只要 Access Token 仍然有效,攻擊者取得後就可能直接拿去呼叫 API。短效期限、Scope 與 Audience 可以縮小影響範圍,但仍無法改變「持有 Token 就可能使用」這件事。
Sender-Constrained Token 要處理的,就是這個問題。它的核心概念是:Resource Server 不只檢查 Access Token 是否有效,也要確認送出請求的 Client 是否持有與 Token 綁定的金鑰。
也就是說,攻擊者即使偷到 Access Token,如果沒有對應的私鑰或憑證,仍然無法完成驗證。
可以把流程整理成三個步驟:
這類「證明自己持有某把私鑰」的設計,稱為 Proof of Possession(PoP,持有證明)。重點不是把私鑰交給伺服器,而是透過簽章或 TLS 握手等方式,讓伺服器確認 Client 真的能使用那把私鑰。
本篇接著介紹兩種常見做法:
💡這裡真正被綁定的是金鑰或憑證,不是
client_id、IP 位址或 User-Agent。
client_id通常是公開識別碼,不能單獨證明呼叫者就是原本的 Client;IP 位址與 User-Agent 可以作為輔助風險訊號,但不能取代密碼學上的持有證明。
mTLS(Mutual TLS,雙向 TLS)的重點是:把 Access Token 綁定到某一張 Client 憑證。之後呼叫 API 時,Resource Server 會同時檢查 Token 是否有效,以及本次連線使用的憑證是否與 Token 綁定資訊相符。
如果攻擊者只偷到 Access Token,卻沒有那張憑證對應的私鑰,就無法建立正確的 mTLS 連線,也就不能直接冒用這顆 Token。

上圖可以分成兩個階段來看。
第一個階段,是 Client 向 Authorization Server 取得 Token:
第二個階段,是 Client 使用 Token 呼叫 Resource Server:
這就是 mTLS 防止 Token 冒用的核心:偷到 Token 還不夠,還必須拿得出 Token 綁定的那張憑證私鑰。
mTLS 在 OAuth 2.0 裡常見有兩種用途:
| 機制 | 發生位置 | 目的 |
|---|---|---|
| mTLS Client Authentication | Client 向 Authorization Server 換 Token 時 | 證明是哪個 Client 來換 Token |
| Certificate-Bound Access Token | Token 核發後,Client 呼叫 Resource Server 時 | 限制這顆 Access Token 只能搭配特定 Client 憑證使用 |
只有在 Token 被綁定到 Client 憑證,而且 Resource Server 也確實比對憑證時,mTLS 才能限制偷來的 Access Token 被冒用。
如果 Access Token 是 JWT,憑證綁定資訊通常會放在 cnf 欄位中。RFC 8705 使用 x5t#S256 表示 Client 憑證的 SHA-256 指紋:
{
"cnf": {
"x5t#S256": "BASE64URL_SHA256_OF_CLIENT_CERTIFICATE"
}
}
也就是說,Token 裡會記著它應該搭配哪一張 Client 憑證使用。
Resource Server 收到請求後,會從 mTLS 連線取得本次 Client 憑證,計算憑證指紋,再和 Token 裡的 x5t#S256 比對。如果不一致,即使 Token 尚未過期,也應拒絕請求。
因此,mTLS 的重點可以整理為:它是透過 TLS 連線中的 Client 憑證,確認拿 Token 來呼叫 API 的 Client 是否符合原本的綁定。 下一節要看的 DPoP,目的也很接近,只是它不靠 TLS Client 憑證,而是改用每次請求產生的簽章證明。
前一節的 mTLS 是透過 TLS 連線中的 Client 憑證,確認呼叫 API 的 Client 是否符合 Token 綁定。DPoP(Demonstrating Proof of Possession)則改用另一種方式:Client 每次送出請求時,都要另外附上一份用私鑰簽署的證明。
DPoP Proof JWT 是 Client 用私鑰簽出來的證明。Resource Server 會用它確認:這次拿 Access Token 呼叫 API 的 Client,是否真的持有綁定的私鑰。

DPoP Header。token_type 會是 DPoP。因此,DPoP 的核心可以整理成一句話:偷到 Access Token 還不夠,攻擊者還必須能用原本綁定的私鑰簽出正確的 Proof。
使用 DPoP 時,API 請求裡會同時出現兩份資料:
| 資料 | 由誰產生 | 主要用途 |
|---|---|---|
| Access Token | Authorization Server 核發 | 表示可以存取哪些資源,並記錄它綁定哪一把公開金鑰 |
| DPoP Proof JWT | Client 使用私鑰簽署 | 證明這次請求是由持有對應私鑰的 Client 發出 |
Access Token 表示授權,DPoP Proof 表示持有證明。兩者要一起檢查,才有辦法限制偷來的 Token 被冒用。
Client 向 Token Endpoint 換 Token 時,會多帶一個 DPoP Header。下面範例中的 DPoP: PROOF_JWT_FOR_TOKEN_REQUEST 這一行,就是 DPoP Header:
POST /token HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: PROOF_JWT_FOR_TOKEN_REQUEST
grant_type=authorization_code&client_id=example-client&code=AUTHORIZATION_CODE&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&code_verifier=PKCE_VERIFIER
如果 Authorization Server 接受 DPoP 綁定,回傳的 Token Type 會是 DPoP:
{
"access_token": "DPOP_BOUND_ACCESS_TOKEN",
"token_type": "DPoP",
"expires_in": 600
}
之後呼叫 API 時,Client 會同時送出 Access Token 與新的 Proof:
GET /payroll HTTP/1.1
Host: api.example.com
Authorization: DPoP DPOP_BOUND_ACCESS_TOKEN
DPoP: NEW_PROOF_JWT_FOR_THIS_API_REQUEST
這裡要注意:
Authorization: DPoP ... 後面放的是 Access Token。DPoP: ... 後面放的是 這次請求專用的 Proof JWT。只把 Header 從 Bearer 改成 DPoP 並不代表請求已具備 DPoP 保護。Access Token 要先記錄它綁定哪一把 Client 金鑰;之後 API 收到請求時,也要確認這次送來的 Proof 是用同一把金鑰簽出來的。
DPoP Proof 是一顆簽署過的 JWT,裡面會放這次請求相關的資訊。可以先從以下幾個欄位理解 Proof 的驗證重點:
| 欄位 | 用途 |
|---|---|
jwk |
Client 的公開金鑰,用來驗證 Proof 簽章 |
htm |
本次 HTTP Method,例如 GET 或 POST |
htu |
本次請求的目標 URI |
iat |
Proof 建立時間 |
jti |
Proof 的唯一編號,可用來偵測重複使用 |
ath |
Access Token 的雜湊,讓 Proof 對應到這次送出的 Token |
Resource Server 收到請求後,會檢查:
其中最重要的是第三點。任何人都可以自己產生一組金鑰並簽出 Proof;只有確認 Proof 的金鑰和 Access Token 綁定的金鑰一致,才能阻止攻擊者用自己的私鑰搭配偷來的 Token。
DPoP 也可能使用 Nonce,但它和前一篇的 OIDC Nonce 不是同一件事。
OIDC Nonce 主要在對應登入流程;DPoP Nonce 主要在降低登入後呼叫 API 時,舊的 DPoP Proof 被重複使用的風險。
mTLS 與 DPoP 都屬於 Sender-Constrained Token,差別在於 Client 要用什麼方式證明自己持有綁定的私鑰。
兩者的差異整理如下:
| 比較項目 | mTLS/Certificate-Bound Token | DPoP |
|---|---|---|
| 核心規格 | RFC 8705 | RFC 9449 |
| 證明方式 | 透過 TLS 握手中的 Client 憑證,證明 Client 持有對應私鑰 | 透過 DPoP Proof JWT,證明 Client 持有對應私鑰 |
| Token 綁定對象 | X.509 Client 憑證 | DPoP 公開金鑰 |
| 常見綁定欄位 | cnf.x5t#S256 |
cnf.jkt |
| 呼叫 API 時怎麼帶 | 使用 mTLS 連線傳送 Access Token,HTTP Header 可沿用 Bearer | 同時帶 Authorization: DPoP 與 DPoP Header |
| 是否需要 Client 憑證 | 需要 | 不需要,使用公私鑰與 JWK |
| 較適合的情境 | 後端服務、企業整合,或已經有憑證管理與 mTLS 基礎設施的系統 | SPA、行動 App,或較適合在應用層處理簽章的系統 |
| 主要成本 | 憑證生命週期、TLS 終止位置、憑證資訊傳遞 | 金鑰保存、每次請求簽署與驗證、Nonce 與 Replay Cache |
mTLS 適合已有憑證管理的系統;DPoP 適合用應用層簽章處理的 Client。
另外,DPoP 仍然需要 HTTPS。它只負責證明 Client 持有私鑰,不能取代 TLS。
mTLS 與 DPoP 能降低 Access Token 外洩後被直接冒用的風險,但並不代表 Token 安全問題已完全解決。實際部署時,仍需留意以下限制。
Sender-Constrained Token 的重點不是取代既有防護,而是在 Token 已經外洩時,多加一道限制。
Access Token 外洩後,系統不能只判斷 Token 是否有效,還要確認目前使用 Token 的 Client 是否符合原本的綁定。
因此,本篇可以整理成三個重點:
Token 外洩後仍保留第二道限制,即使攻擊者取得 Access Token,也必須同時具備對應的憑證或私鑰,才有可能完成 API 存取。
下一篇會接著討論重放攻擊、時序攻擊與競態條件,說明驗證流程中為什麼不能只看「資料是否有效」,也要確認請求是否符合當下的時間、狀態與一次性使用條件。